
exports.Widget = Widget;

const wid = require("widgeon");

function Widget () {}
Widget.prototype = {
    locales:    ["*"],
    setLocales:    function (locs) {
        this.locales = [];
        for (var i = 0; i < locs.length; i++) {
            var loc = locs[i];
            // XXX need to also handle deprecated ones
            if (loc.indexOf('*') == 0 || loc.indexOf('i') == 0 || /\s/.test(loc)) continue;
            loc = loc.split(/-\*/).join("");
            var subtags = loc.split(/-/);
            while (subtags.length > 0) {
                this.locales.push(subtags.join("-"));
                subtags.pop();
            }
        }
        this.locales.push("*");
    },
    
    zip:        null,
    uuid:       null,
    rootURI:    null,
    initWithUUID:    function (uuid) {
        this.uuid = uuid;
        this.zip = wid.getPackage(uuid);
        var file = wid.widDir.clone();
        file.append(this.uuid + ".wgt");
        this.rootURI = "jar:" + wid.fileURI(file) + "!/";
        return this;
    },

    initWithFile:    function (file, id) {
        this.uuid = id;
        this.zip = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
        try {
            this.zip.open(file);
        }
        catch (e) {
            throw("Widgeon failed to open zip archive");
        }
        this.rootURI = "jar:" + wid.fileURI(file) + "!/";
        wid.cacheWidget(id, this); // XXX hack!
        return this;
    },
    
    cfg:    {
        authorEmail:    null,
        authorHref:     null,
        authorName:     null,
        features:       null,
        icons:          null,
        description:    null,
        id:             null,
        license:        null,
        licenseHref:    null,
        preferences:    null,
        name:           null,
        shortName:      null,
        version:        null,
        width:          null,
        height:         null,
        viewModes:      null,
        startFile:      null,
        startFileEnc:   "UTF-8", // not sure we can know that
        startFileMedia: "text/html"
    },
    supportedFeat:  {},
    cfgDone:    false,
    config:     function () {
        //if (this.cfgDone) return this.cfg;
        let z = this.zip, cfg = this.cfg;
        if (!z.hasEntry("config.xml")) throw("No config.xml in widget package");
        
        // XXXX clear this.cfg  - strange bug
        for (let i in cfg) cfg[i] = null;
        cfg.startFileEnc = "UTF-8";
        cfg.startFileMedia = "text/html";
        
        // load the XML
        var sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream);
        sis.init(z.getInputStream("config.xml"));
        var parser = Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
        var doc = parser.parseFromString(sis.read(sis.available()), "text/xml");
        
        // --- Step 7 ---
        // process <widget>
        var ignored = {};
        var root = doc.documentElement;
        var NS = "http://www.w3.org/ns/widgets";
        if (root.namespaceURI != NS || root.localName != "widget") throw("Document isn't rooted in <widget>.");
        // XXX need to validate that it's an IRI
        var id = this.singleAttr(root, "id");
        if (id.length && this.validateIRI(id)) cfg.id = id;
        if (root.hasAttributeNS(null, "version")) cfg.version = this.singleAttr(root, "version");
        if (root.hasAttributeNS(null, "height")) cfg.height = this.nonNegInt(root.getAttributeNS(null, "height"));
        if (root.hasAttributeNS(null, "width")) cfg.width = this.nonNegInt(root.getAttributeNS(null, "width"));
        if (width) cfg.width = width;
        var tempModes = this.keywordAttr(root.getAttributeNS(null, "viewmodes"));
        var suppModes = {
            application:    true,
            floating:       true,
            fullscreen:     true,
            mini:           true
            // maximised:      true, // XXX not in spec yet
        };
        var seenModes = {};
        var modes = [];
        for (var i = 0; i < tempModes.length; i++) {
            var m = tempModes[i]
            if (!suppModes[m]) continue;
            if (seenModes[m]) continue;
            seenModes[m] = true;
            modes.push(m);
        }
        if (modes.length) cfg.viewModes = modes;

        // process elements
        var elems = [];
        for (var i = 0; i < this.locales.length; i++) {
            var loc = this.locales[i];
            if (loc == "*") {
                var el = root.firstElementChild;
                while (el) {
                    if (el.namespaceURI == NS) {
                        var lang = this.getLang(el);
                        if (!lang) elems.push(el);
                    }
                    el = el.nextElementSibling;
                }
            }
            else {
                // XXX we need to apply BCP47 lookup in here, according to spec (maybe we do already)
                var el = root.firstElementChild;
                while (el) {
                    if (el.namespaceURI == NS) {
                        if (el.localName == "name" || el.localName == "description" || el.localName == "license") {
                            var lang = this.getLang(el);
                            if (lang == loc) elems.push(el);
                        }
                    }
                    el = el.nextElementSibling;
                }
            }
        }
        var seenEl = {};
        for (var i = 0; i < elems.length; i++) {
            var el = elems[i];
            var ln = el.localName;
            if (ln == "name") {
                if (seenEl[ln]) continue;
                seenEl[ln] = true;
                cfg.name = this.normTextContent(el);
                if (el.hasAttributeNS(null, "short")) cfg.shortName = this.singleAttr(el, "short");
            }
            else if (ln == "description") {
                if (seenEl[ln]) continue;
                seenEl[ln] = true;
                cfg.description = this.textContent(el);
            }
            else if (ln == "license") {
                if (seenEl[ln]) continue;
                seenEl[ln] = true;
                cfg.license = this.textContent(el);
                // XXX need some validation of href/path
                if (el.hasAttributeNS(null, "href")) cfg.licenseHref = this.singleAttr(el, "href");
            }
            else if (ln == "icon") {
                if (!el.hasAttributeNS(null, "src")) continue;
                var src = this.hasFile(this.singleAttr(el, "src"));
                if (!src) continue;
                if (!cfg.icons) cfg.icons = [];
                var width, height;
                if (el.hasAttributeNS(null, "height")) height = this.nonNegInt(el.getAttributeNS(null, "height"));
                if (el.hasAttributeNS(null, "width")) width = this.nonNegInt(el.getAttributeNS(null, "width"));
                cfg.icons.push({
                    src:    src, // note that this is a resolved path
                    width:  width,
                    height: height
                });
            }
            else if (ln == "author") {
                if (seenEl[ln]) continue;
                seenEl[ln] = true;
                cfg.authorName = this.normTextContent(el);
                if (el.hasAttributeNS(null, "href")) {
                    var ah = this.singleAttr(el, "href");
                    if (this.validateIRI(ah)) cfg.authorHref = ah;
                }
                if (el.hasAttributeNS(null, "email")) cfg.authorEmail = this.singleAttr(el, "email");
            }
            else if (ln == "preference") {
                var name = this.singleAttr(el, "name");
                if (!name) continue;
                if (!cfg.preferences) cfg.preferences = {};
                if (typeof(cfg.preferences[name]) != "undefined") continue;
                var pref = {
                    name:       name,
                    value:      this.singleAttr(el, "value"),
                    readonly:   (this.singleAttr(el, "readonly") == "true") ? true : false
                };
                cfg.preferences[name] = pref;
            }
            else if (ln == "content") {
                if (seenEl[ln]) continue;
                seenEl[ln] = true;
                if (!el.hasAttributeNS(null, "src")) continue;
                var src = this.hasFile(this.singleAttr(el, "src"));
                if (!src) continue;
                cfg.startFile = src; // resolved path
                if (el.hasAttributeNS(null, "type")) {
                    var type = this.singleAttr(el, "type");
                    if (!wid.supportsType(type)) throw("Widgeon does not support type: " + type);
                    cfg.startFileMedia = type;
                }
                if (el.hasAttributeNS(null, "encoding")) {
                    var enc = this.singleAttr(el, "encoding");
                    if (wid.supportsEncoding(enc)) cfg.startFileEnc = enc;
                }
            }
            else if (ln == "feature") {
                var name = this.singleAttr(el, "name");
                if (!name) continue; // XXX need to validate that it's an IRI
                var req = true;
                if (el.hasAttributeNS(null, "required") && this.singleAttr(el, "required") == "false") req = false;
                // XXX we don't support any features as of yet :)
                if (req && !this.supportedFeat[name]) {
                    // XXX we'll need to do better than that
                    throw("Widgeon does not support feature: " + name);
                }
                var feat = {
                    name:       name,
                    required:   req,
                    params:     []
                };
                var prm = el.firstElementChild;
                while (prm) {
                    if (prm.namespaceURI == NS && prm.localName == "param" && 
                        prm.hasAttributeNS(null, "name") && prm.hasAttributeNS(null, "value")) {
                        var k = this.singleAttr(prm, "name");
                        if (k != "") {
                            var v = this.singleAttr(prm, "value");
                            feat.params.push({ name: k, value: v });
                        }
                    }
                    prm = prm.nextElementSibling;
                }
                if (!cfg.features) cfg.features = [];
                if (this.supportedFeat[name]) cfg.features.push(feat);
            }
        }

        // -- Step 8 ---
        if (!cfg.startFile) {
            var ext = ["htm", "html", "svg", "xhtml", "xht"];
            for (var i = 0; i < ext.length; i++) {
                var f = this.hasFile("index." + ext[i]);
                if (f) {
                    cfg.startFile = f;
                    break;
                }
            }
            if (!cfg.startFile) {
                throw("No start file in widget.");
            }
        }

        // -- Step 9 ---
        var ext = ["svg", "ico", "png", "gif", "jpg"];
        for (var i = 0; i < ext.length; i++) {
            var f = this.hasAllFiles("icon." + ext[i]);
            if (f) {
                if (!cfg.icons) cfg.icons = [];
                for (var j = 0; j < f.length; j++) {
                    var src = f[j];
                    if (!cfg.icons.some(function (ic) { return ic.src == src })) cfg.icons.push({ src: src });
                }
            }
        }
        
        this.cfgDone = true;
        return cfg;
    },
    
    file:    function (path) {
        var locPath = this.hasFile(path);
        if (locPath) {
            return this.getURI(locPath);
        }
        else {
            return null;
        }
    },

    getURI:     function (path) {
        return this.rootURI + path;
    },

    hasFile:    function (path) {
        // return the localised path if it exists, nothing otherwise
        if (path.indexOf("/") == 0) path.replace(/^\//, "");
        if (path == "") return null;
        var steps = path.split("/");
        if (steps[0] == "locales") {
            if (steps.length == 1) return null;
            if (!/^[a-z]{1,8}(?:-[a-z]{1,8})?$/.test(steps[1])) return null;
        }
        for (var i = 0; i < this.locales.length; i++) {
            var loc = this.locales[i];
            if (loc == "*") continue;
            var locPath = "locales/" + loc + "/" + path;
            if (this.zip.hasEntry(locPath)) {
                var ent = this.zip.getEntry(locPath);
                if (ent.isDirectory) return null;
                else return locPath;
            }
        }
        if (this.zip.hasEntry(path)) {
            var ent = this.zip.getEntry(path);
            if (ent.isDirectory) return null;
            else return path;
        }
        return null;
    },

    hasAllFiles:    function (path) {
        var files = [];
        // return the localised path if it exists, nothing otherwise
        if (path.indexOf("/") == 0) path.replace(/^\//, "");
        var steps = path.split("/");
        if (steps[0] == "locales") {
            if (steps.length == 1) return null;
            if (!/^[a-z]{1,8}(?:-[a-z]{1,8})?$/.test(steps[1])) return null;
        }
        for (var i = 0; i < this.locales.length; i++) {
            var loc = this.locales[i];
            if (loc == "*") continue;
            var locPath = "locales/" + loc + "/" + path;
            if (this.zip.hasEntry(locPath)) {
                var ent = this.zip.getEntry(locPath);
                if (ent.isDirectory) continue;
                else files.push(locPath);
            }
        }
        if (this.zip.hasEntry(path)) {
            var ent = this.zip.getEntry(path);
            if (!ent.isDirectory) files.push(path);
        }
        if (files.length > 0) return files;
        return null;
    },

    win:    null,
    open:    function () {
        var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].getService(Ci.nsIWindowWatcher);
        var size = "width=" + (this.cfg.width ? this.cfg.width : 500) + ",height=" + (this.cfg.height ? this.cfg.height : 600) + ",";
        // XXX this is the code to use when I've figured out how to make Widget URIs work in JetPack
        this.win = ww.openWindow(null, "widget://" + this.uuid + "/" + this.cfg.startFile, "_blank", size + "chrome,centerscreen,resizable,dialog=no", null);
        // XXX this is using jar as a stop gap!
        // this.rootURI = "jar:" + wid.fileURI(file) + "!/";
        // this.win = ww.openWindow(null, this.rootURI + this.cfg.startFile, "_blank", size + "chrome,centerscreen,resizable,dialog=no", null);
        // XXX inject the API
        // this.win.widget = new WidgetAPI(this.cfg);
        return this.win;
    },


    // --- P+C processing rules ---
    // moz should handle this, but doesn't seem to in tests
    ws:     "[\\u0009-\\u000d\\u0020\\u0085\\u00a0\\u1680\\u180e\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000]",
    textContent:    function (el) {
        return el.textContent;  // XXX ITS support plugs in here
    },
    
    normTextContent:    function (el) {
        return this.trim(this.textContent(el));
    },
    
    getLang:    function (el) {
        var XML_NS = "http://www.w3.org/XML/1998/namespace";
        var lang = el.getAttributeNS(XML_NS, "lang");
        if (lang) return lang;
        if (el.parentNode && el.parentNode.nodeType == 1) return this.getLang(el.parentNode);
        return null;
    },
    
    singleAttr:    function (el, at) {
        return this.trim(el.getAttributeNS(null, at));
    },
    
    nonNegInt:    function (str) {
        var res = 0;
        if (str.length == 0) return null;
        if ((new RegExp("^" + this.ws + "+$")).test(str)) return null;
        var s = new RegExp(this.ws);
        for (var i = 0; i < str.length; i++) {
            var ch = str[i];
            if (s.test(ch)) continue;
            if (/[^0-9]/.test(ch)) return res;
            res *= 10;
            res += (1*ch);
        }
        return res;
    },
    
    trim:    function (str) {
        if (!str) return "";
        // return str.replace(/^\s+/, "").replace(/\s+$/, "").split(/\s+/).join(" ");
        var splus = this.ws + "+";
        return str.replace(new RegExp("^" + splus), "").replace(new RegExp(splus + "$"), "").split(new RegExp(splus)).join(" ");
    },
    
    keywordAttr:    function (str) {
        str = this.trim(str);
        return str.split(" ");
    },
    
    // XXX this is not entirely correct, but validating IRIs is stupid anyway
    validateIRI:    function (iri) {
        let ioService = Cc["@mozilla.org/network/io-service;1"]  .getService(Ci.nsIIOService);  
        try {
            let uri = ioService.newURI(iri, null, null);
            return true;
        }
        catch (e) {
            return false;
        }
    }

};

// XXX need to use COW here for this to work
//
// function WidgetAPI (cfg) {
//     this.cfg = cfg;
// }
// 
// WidgetAPI.prototype = {
//     // these are just the basic fields
//     get author      () { return this.cfg.authorName; },
//     get authorEmail () { return this.cfg.authorEmail; },
//     get authorHref  () { return this.cfg.authorHref; },
//     get description () { return this.cfg.description; },
//     get id          () { return this.cfg.id; },
//     get name        () { return this.cfg.name; },
//     get shortName   () { return this.cfg.shortName; },
//     get version     () { return this.cfg.version; },
// 
//     // dynamic size of the window (who needs this, seriously)
//     get height  () { return this.win.innerHeight; },
//     get width   () { return this.win.innerWidth; },
// 
//     // XXX needs to be some sort of Storage (maybe we can emulate?)
//     get preferences () { throw("preferences not implemented yet"); },
//     
//     openURL:    function (url) {
//         // XXX
//         throw("openURL not implemented yet");
//     },
// };
